Swiss E-ID: Issuing Credentials

In our previous post, we explored the verification side of the Swiss E-ID by setting up and testing the swiyu-verifier service. In this follow-up, we'll look at the other half of the ecosystem: credential issuance. We will deploy the official swiyu-issuer service locally, issue a custom Verifiable Credential (VC), and then test how our verifier from Part 1 handles credentials from different issuers.
This post is the first in a series:
Part 1: Issuer Service Setup
To begin, we need to subscribe to the swiyucorebusiness_status API using our partner ID, in addition to the APIs from Part 1. This is required for managing credential status lists (i.e., revocation).
First, let's download the Docker configuration for the local issuer service:
curl [https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-issuer/refs/heads/main/sample.compose.yml](https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-issuer/refs/heads/main/sample.compose.yml) > sample.compose.yml
curl [https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-issuer/refs/heads/main/.env](https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-issuer/refs/heads/main/.env) > .env
Before we can issue credentials, we must initialize a Status List. This list is used to manage the revocation status of the credentials we issue.
curl -X POST http://localhost:8080/management/api/status-list \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"type": "TOKEN_STATUS_LIST",
"maxLength": 100000,
"config": {
"bits": 2
}
}'
The response will include a statusRegistryUrl (e.g., .../api/v1/statuslist/cbc26c47...jwt). We will embed this URL into every credential we issue.
Part 2: Issuing a Custom Credential "my-test-vc"
With the service running (e.g., via docker compose up and ngrok), we can now offer a credential. This process follows the OID4VCI (OpenID for Verifiable Credential Issuance) protocol.
Let's issue a simple VC with a custom type, my-test-vc:
curl -X POST http://localhost:8080/management/api/credentials \
-H "accept: */*" \
-H "Content-Type: application/json" \
-d '{
"metadata_credential_supported_id": [
"my-test-vc"
],
"credential_subject_data": {
"firstName": "Test FirstName",
"lastName": "Test LastName",
"birthDate": "01.01.2025"
},
"offer_validity_seconds": 86400,
"credential_valid_until": "2030-01-01T19:23:24Z",
"credential_valid_from": "2025-01-01T18:23:24Z",
"status_lists": [
"[https://status-reg.trust-infra.swiyu-int.admin.ch/api/v1/statuslist/cbc26c47-2d0d-4292-9e9f-6718173113a8.jwt](https://status-reg.trust-infra.swiyu-int.admin.ch/api/v1/statuslist/cbc26c47-2d0d-4292-9e9f-6718173113a8.jwt)"
]
}' > vc_offer.json
# Generate QR code from the response deeplink
qrencode -t ANSIUTF8 $(jq -r '.offer_deeplink' vc_offer.json)
Scanning this QR code with the swiyu app triggers the OID4VCI flow. By monitoring the ngrok traffic, we can observe the wallet making several requests:
GET /.well-known/openid-credential-issuerGET /.well-known/oauth-authorization-serverPOST /oid4vci/api/tokenPOST /oid4vci/api/credential
The final call delivers the vc+sd-jwt to the wallet, which has our custom data and the vct (Verifiable Credential Type) of my-test-vc. We can then use our verifier from Part 1 to request and successfully validate this new credential.
Part 3: Testing with a Conflicting Credential Type "betaid-sdjwt"
As a technical exercise, let's explore the system's boundaries. What happens if our local issuer issues a credential that uses the same vct as the official beta ID: betaid-sdjwt?
First, we must update the openid_metadata in our sample.compose.yml to advertise that our issuer "supports" this credential type.
Next, we issue the new credential, populating the claims that the beta ID expects (like family_name and given_name):
curl -X POST http://localhost:8080/management/api/credentials \
-H "accept: */*" \
-H "Content-Type: application/json" \
-d '{
"metadata_credential_supported_id": [
"betaid-sdjwt"
],
"credential_subject_data": {
"family_name": "DontTrustThis FamilyName",
"given_name": "DontTrustThis GivenName",
"birthDate": "01.01.2025",
"expiry_date": "2026-02-10"
},
"offer_validity_seconds": 86400,
"credential_valid_until": "2030-01-01T19:23:24Z",
"credential_valid_from": "2025-01-01T18:23:24Z",
"status_lists": [
"https*://status-reg.trust-infra.swiyu-int.admin.ch/api/v1/statuslist/cbc26c47-2d0d-4292-9e9f-6718173113a8.jwt"
]
}' > vc_offer-bcs.json
# Generate the QR code
qrencode -t ANSIUTF8 $(jq -r '.offer_deeplink' vc_offer-bcs.json)
We can scan and import this credential into the swiyu wallet just as easily as the first one.
Now, let's use our local verifier service from Part 1. We'll use its original request file (vs_request-bcs.json) which is configured to ask for a betaid-sdjwt.
# 1. Create a verification request for a 'betaid-sdjwt'
curl -X POST http://localhost:8083/management/api/verifications \
-H "Content-Type: application/json" \
--data-binary @vs_request-bcs.json > vs_request_response-bcs.json
# 2. Generate the verification QR code
qrencode -t ANSIUTF8 $(jq -r '.verification_url' vs_request_response-bcs.json)
# 3. Poll the verifier for the result
curl "http://localhost:8083/management/api/verifications/$(jq -r '.id' vs_request_response-bcs.json)" > vs_result-bcs.json
# 4. Check the verified data
jq -r ".wallet_response.credential_subject_data.family_name" vs_result-bcs.jsonResult: DontTrustThis FamilyName
Our local verifier service successfully accepted the credential from our local issuer. This highlights a configuration detail in the swiyu-verifier service. The verification was successful because the default configuration for accepted_issuer_dids in the vs_request.json was an empty array ([]). In the swiyu-verifier service, this setting appears to default to trusting any issuer. The verifier only checked that the vct matched (betaid-sdjwt) and that the credential's signature was valid, but it did not validate who the issuer was.
This is a great example of why secure per default configuration is important, especially for such a critical system as an electronic goverment id. The official cookbook from the Swiss government has an example with [] that is insecure. There is only a warning at the top that the "security of the system remains under development".
However now people are implementing prototypes, which will be the basis of future production systems. I would not be surprised if one or the other verifying service would be vulnerable to this attack.
In a production environment, this default behavior would be a critical security risk. It would allow any party to issue a credential and a verifier with this default configuration would incorrectly trust it.
For any production system, the accepted_issuer_dids array must be explicitly populated with the DIDs of the issuers you intend to trust (e.g., the official DID of the Beta Credential Service).

We can confirm that (fortunately) the official BCS verification portal (bcs.admin.ch) is configured correctly. When presented with our test credential to it, it is properly rejecting the credential with a message indicating the issuer was not trusted.
Update: The team behind the Swiss E-ID acknowledged the issue and put a fix on their roadmap. https://github.com/orgs/swiyu-admin-ch/discussions/43. To be tested again in 2026.

Gian-Luca Frei
Gian-Luca Frei is security engineer and specialist for login and authentication. He has a proven track record of securing systems with the highest security standards, including e-banking portals and health applications.
He previously spent 6 years at Zühlke as a security consultant, working in a highly international setting across Switzerland, Singapore, and Hong Kong.
Gian-Luca is also the founder and co-leader of the OWASP Application Gateway Project.
He has a keen interest in modern cryptographic protocols, and his contributions were recognized with the ISSS Excellence Award in 2019.
Our latest article
Offer Your User Secure and Easy Login Experinces?
Ready to elevate your very first user touchpoin? Contact us today and transform your business with better user experiences.
